package com.xlp;

import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlOptions;
import org.apache.xmlbeans.impl.store.DomImpl;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * 书签替换处理类
 * 参考于<link>https://blog.csdn.net/catoop/article/details/124198271</link
 * @author xlp
 */
public class BookmarkService {

    /**
     * 因为docx为xml格式的结构，一下为docx中定义的部分常量引用
     **/
    public static final String RUN_NODE_NAME = "w:r";
    public static final String TEXT_NODE_NAME = "w:t";
    public static final String BOOKMARK_START_TAG = "w:bookmarkStart";
    public static final String BOOKMARK_END_TAG = "w:bookmarkEnd";
    public static final String NODENAME_PARAGRAPH = "w:p";
    public static final String BOOKMARK_ID_ATTR_NAME = "w:name";
    public static final String PARAGRAPH_PROPERTIES_NAME = "w:pPr";

    /**
     * 读取 docx 文件中的所有书签（注意不支持书签嵌套，书签嵌套书签的情况只识别最外层书签）
     *
     * @param docx
     */
    public List<Node> getBookmarksFromDocx(XWPFDocument docx) throws OpenXML4JException {
        Node bodyNode = docx.getDocument().getBody().getDomNode();

        // 递归读取 bookmarkStart 节点，返回bookmarkNode集合（<w:bookmarkStart w:id="1" w:name="书签名称"/>）
        List<Node> bookmarkNodeList = new ArrayList<>();
        //获取文档主内容中的书签
        getBookmarksFromNode(bodyNode, bookmarkNodeList);
        //获取页脚中的书签
        getFooterBookmarks(docx, bookmarkNodeList);
        //获取也没中的书签
        getHeaderBookmarks(docx, bookmarkNodeList);

        return bookmarkNodeList;
    }

    private void getHeaderBookmarks(XWPFDocument docx, List<Node> bookmarkNodeList) {
        List<XWPFHeader> headerList = docx.getHeaderList();
        for (XWPFHeader header : headerList) {
            getParagraphAndTableBookmarks(bookmarkNodeList, header.getListParagraph(), header.getTables());
        }
    }

    private void getFooterBookmarks(XWPFDocument docx, List<Node> bookmarkNodeList) {
        List<XWPFFooter> footerList = docx.getFooterList();
        for (XWPFFooter xwpfFooter : footerList) {
            getParagraphAndTableBookmarks(bookmarkNodeList, xwpfFooter.getListParagraph(), xwpfFooter.getTables());
        }
    }

    private void getParagraphAndTableBookmarks(List<Node> bookmarkNodeList, List<XWPFParagraph> listParagraph,
                                               List<XWPFTable> tables) {
        for (XWPFParagraph xwpfParagraph : listParagraph) {
            CTP ctp = xwpfParagraph.getCTP();
            getBookmarksFromNode(ctp.getDomNode(), bookmarkNodeList);
        }

        for (XWPFTable table : tables) {
            CTTbl ctTbl = table.getCTTbl();
            getBookmarksFromNode(ctTbl.getDomNode(), bookmarkNodeList);
        }
    }

    /**
     * 递归解析所有Node节点，将bookmark记录到集合中
     *
     * @param node
     * @param bookmarkNodeList
     */
    public void getBookmarksFromNode(Node node, List<Node> bookmarkNodeList) {
        if (node.getNodeName().equals(BOOKMARK_START_TAG)) {
            bookmarkNodeList.add(node);
        } else if (node.getNodeName().equals(BOOKMARK_END_TAG)
                || node.getNodeName().equals(PARAGRAPH_PROPERTIES_NAME)) {
            // Nothing
        } else {
            NodeList childNodes = node.getChildNodes();
            for (int i = 0, j = childNodes.getLength(); i < j; i++) {
                Node childNode = childNodes.item(i);
                getBookmarksFromNode(childNode, bookmarkNodeList);
            }
        }
    }


    /**
     * 将一个docx文档替换到docx的书签中
     * 1.要求书签尽量设置在文档的换行起点
     * 2.被合入的docx会作为换行起点开始合入，如果bookmark没有设置在一个换行的起点，程序会自动从该bookmark节点开始寻找，
     * 定位到父节点为body的对应p节点的下一个节点，然后将需要合入的文档所有内容追加到该p节点的下一个
     *
     * @param bookmarkParentParagraph
     * @param startBookmarkNode
     * @param content
     */
    public void replaceDocxBookmarkFromString(XWPFParagraph bookmarkParentParagraph, Node startBookmarkNode, String content) {
        Node nextNode = startBookmarkNode.getNextSibling();
        boolean contentReplaced = false;
        while (nextNode != null) {
            // 节点是结束符
            if (nextNode.getNodeName().equals(BOOKMARK_END_TAG)
                || nextNode.getNodeName().equals(BOOKMARK_START_TAG)) {
                break;
            }
            // 1.寻找startBookmark的下一个 w:r 节点，然后将该节点中的 w:t 节点的真正文字内容替换掉（这样可以保留原来的bookmark的文字样式）
            if (!contentReplaced && nextNode.getNodeName().equals(RUN_NODE_NAME)) {
                NodeList runChildNodes = nextNode.getChildNodes();
                for (int i = 0, j = runChildNodes.getLength(); i < j; i++) {
                    if (runChildNodes.item(i).getNodeName().equals(TEXT_NODE_NAME)) {// w:t 是真正的文本内容
                        runChildNodes.item(i).getFirstChild().setNodeValue(content);
                        break;
                    }
                }
                contentReplaced = true;
                nextNode = nextNode.getNextSibling();
            } else if(nextNode.getNodeName().equals(RUN_NODE_NAME)){
                // 2.然后继续向下删除 endBookmark或下一个startBookmark之前的所有w:r节点
                Node tempNode = nextNode.getNextSibling();
                startBookmarkNode.getParentNode().removeChild(nextNode);
                nextNode = tempNode;
            }
        }
        //1-end

        if (!contentReplaced) {
            XWPFRun run = bookmarkParentParagraph.createRun();
            run.setText(content);
            Node newChildNode = run.getCTR().getDomNode();
            Node startBookmarkNextNode = startBookmarkNode.getNextSibling();
            if (startBookmarkNextNode == null) {
               Node s = startBookmarkNode.getParentNode();
               if (s != null) s.appendChild(newChildNode);
            } else {
                startBookmarkNode.getParentNode().insertBefore(newChildNode, startBookmarkNextNode);
            }
        }
    }



    /**
     * 向上递归获得指定node节点的上一个p节点
     *
     * @param node
     * @return
     */
    public XWPFParagraph getFirstParentParagraphByNode(Node node, XWPFDocument docx) throws XmlException {
        XmlOptions options = new XmlOptions();
        options.setUseSameLocale(((DomImpl.Dom) node).locale());
        Node firstParentNodeByNode = getFirstParentNodeByNode(node);
        if (firstParentNodeByNode == null) return null;
        return new XWPFParagraph(CTP.Factory.parse(firstParentNodeByNode, options), docx);
    }

    /**
     * 向上递归获得指定node节点的上一个p节点
     *
     * @param node
     * @return
     */
    public Node getFirstParentNodeByNode(Node node) throws XmlException {
        Node parentNode = node.getParentNode();
        if (parentNode == null || NODENAME_PARAGRAPH.equals(parentNode.getNodeName())) {
            return parentNode;
        }
        return getFirstParentNodeByNode(parentNode);
    }

    public static void main(String[] args) throws Exception {
        BookmarkService service = new BookmarkService();
        FileInputStream inputStream = new FileInputStream("G:\\docTest\\123.docx");
        XWPFDocument wordDoc = new XWPFDocument(inputStream);
        List<Node> nodes = service.getBookmarksFromDocx(wordDoc);
        inputStream.close();
        for (Node node : nodes) {
            NamedNodeMap attributes = node.getAttributes();
            String nodeValue = attributes.getNamedItem(BOOKMARK_ID_ATTR_NAME).getNodeValue();
            System.out.println(nodeValue);
            /*if ("id".equals(nodeValue)){
                XWPFParagraph paragraph = service.getFirstParentParagraphByNode(node, wordDoc);
                service.replaceDocxBookmarkFromString(paragraph, node, "dd的");
            }*/
            XWPFParagraph paragraph = service.getFirstParentParagraphByNode(node, wordDoc);
            if (paragraph == null) paragraph = wordDoc.createParagraph();
            service.replaceDocxBookmarkFromString(paragraph, node, "【解决的--" + nodeValue + "】");
        }
        wordDoc.write(new FileOutputStream("G:\\docTest\\1234.docx"));
    }
}
